/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.server;

import java.io.IOException;
import java.security.Principal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.javaongems.runtime.http.ExpressRedirector;
import org.javaongems.runtime.util.PropertiesUtils;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * <p>Provides a servlet that enables service implementations and actions to 
 * be plugged in and invoked under a context name.</p>
 * <p>Responsibilities:
 * <ul>
 * 	<li> load all service implementation on initialisations
 * 	<li> load all action implementation on initialisations
 * 	<li> make the Http servlet request available as a static thread local
 * 	<li> make the Http principal available (regardless of a secured context) as 
 * 		a static thread local
 * 	<li> get the service implementation for invocation by the RemoveServiceServlet
 * 	<li> get and invoke the http post action
 * </ul>
 * </p>
 * <p>Collaborators:
 * <ul>
 * 	<li> RemoteServiceServlet - hacked to enable this class to override behaviour
 * </ul>
 * </p>
 * <p>Constraints:
 * <ul>
 * 	<li> service implementations must be written to be thread safe.
 * </ul>
 * </p>
 * @author Ashin Wimalajeewa (ash)
 */
/**
 * @author ash
 *
 */
public class GemServe extends RemoteServiceServlet {
	final static private Logger LOGGER = Logger.getLogger(GemServe.class);

	final static public String WILD_CARD_SPECIFIER = "*";
    final static public String SERVICE_CTX = "service.contexts";
    final static public String ACTION_CTX = "action.contexts";

    final static private long serialVersionUID = 1L;
	final static private ThreadLocal<HttpServletRequest> threadsRequest = new ThreadLocal<HttpServletRequest>();
	final static private ThreadLocal<Principal> threadsPrincipal = new ThreadLocal<Principal>();
	
	private Map<String, RemoteService> serviceContexts = new HashMap<String, RemoteService>();
	private Map<String, Action> actionContexts = new HashMap<String, Action>();
	private Map<String, Action> wildCardActionContexts = new HashMap<String, Action>();

	/**
	 * Called from the servlet container to initialise services & actions on 
	 * itself before use. The following servlet configuration parameters are required:
	 * <ul>
	 * 	<li> service.contexts
	 * 	<li> action.contexts
	 * </ul>
	 * Note, action.contexts support wildcards.  
	 * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		String serviceDetails = config.getInitParameter(SERVICE_CTX);
		if (serviceDetails == null)
			throw new ServletException("GemServe requires service.contexts parameter to be set with valid context/implementations");
		
		Properties svcProps = PropertiesUtils.toProperties(serviceDetails);
		loadServiceContextImplementations(svcProps, config);
		
		String actionDetails = config.getInitParameter(ACTION_CTX);
		Properties acnProps = PropertiesUtils.toProperties(actionDetails);
		loadActionContextImplementations(acnProps, config);
		
		LOGGER.info("GemService " + SERVICE_CTX + "[" + serviceDetails + "]");
		LOGGER.info("GemService " + ACTION_CTX + "[" + actionDetails + "]");
	}

	// HttpServlet override
	/**
	 * Responsilibities:
	 * <ul>
	 * 	<li> initialises and finally clears the http request and principal thread locals
	 * 	<li> determines if the http post is an action implementation that is registered
	 * 		in the action.contexts, and if so invokes that
	 * 	<li> otherwise pass control on to Google's RemoteServiceServlet
	 * </ul>
	 * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		threadsRequest.set(req);
		threadsPrincipal.set(PrincipalUtils.getPrincipal(req));
		try {
			super.service(req, resp);
		} finally {
			threadsRequest.set(null);
			threadsPrincipal.set(null);
		}
	}
	
	/**
	 * Changed the nature of Google's RemoteServiceServlet to support context
	 * based posts (ie. non-service posts). This way GemServe is able to
	 * support all "post" related traffic.
	 * 
	 * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		String subCtx = getGemServeSubcontext(req);
		Action action = getActionImplForContext(subCtx);
		if (action != null)
			executeAction(req, resp, subCtx, action);
		else
			super.doPost(req, resp);
	}
	
	/**
	 * Query the action for a given subcontext by checking:
	 * <ol>
	 * 	<li> whether any wild card entries match the subcontext
	 * 	<li> the action.contexts container a match for the subcontext
	 * </ol>
	 * @param subCtx - the sub context to check on
	 * @return the action associated with the sub context
	 */
	protected Action getActionImplForContext(String subCtx) {
		Action ret = null;
		for (Iterator iter = wildCardActionContexts.keySet().iterator(); iter.hasNext();) {
			String ctx = (String) iter.next();
			if (subCtx.startsWith(ctx)) {
				ret = wildCardActionContexts.get(ctx);
				return ret;
			}
		}
		ret = actionContexts.get(subCtx);
		return ret;
	}
	
	/**
	 * Invokes the execute() method on the action implementation.
	 * @param req - the http request to pass to the action's execution
	 * @param resp - the http response to pass to the action's execution
	 * @param ctx - - the sub-context that was used to resolve the action
	 * @param action - the action to invoke on
	 * @throws ServletException
	 * @throws IOException
	 */
	private void executeAction(HttpServletRequest req, HttpServletResponse resp, String ctx, Action action) throws ServletException, IOException {
		if (LOGGER.isDebugEnabled()) LOGGER.debug("invoking action.ctx [" + ctx + "] "+ action);
		try {
			action.execute(req, resp);
		} catch (ExpressRedirector.ShortCircuit err) {
			// ignore this
		}
	}
	
	// RemoteServiceServlet overrides
	/**
	 * Used to prevent Google's implementation from checking whether the 
	 * service is implemented by this servlet.
	 * 
	 * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#isImplementedRemoteServiceInterface(java.lang.String)
	 */
	protected boolean isImplementedRemoteServiceInterface(String intfName) {
		return true;
	}
	
	/**
	 * Used to override the target service implementation by redirecting it
	 * to the entry resolved from service.contexts.
	 * 
	 * @see com.google.gwt.user.server.rpc.RemoteServiceServlet#getObjectToInvokeServiceIntfMethodOn()
	 */
	protected Object getObjectToInvokeServiceIntfMethodOn() {
		HttpServletRequest request = getThreadLocalRequest();
		String pathInfo = getGemServeSubcontext(request);
		RemoteService ret = serviceContexts.get(pathInfo);
		if (LOGGER.isDebugEnabled()) LOGGER.debug("invoking service.ctx [" + pathInfo + "] " + ret);
		return ret;
	}
	
	/**
	 * Strips the first character (ie. slash) from the http request's path info
	 * value.
	 * @param req - the http request to query the path info from
	 * @return the subcontext of the request
	 */
	private String getGemServeSubcontext(HttpServletRequest req) {
		String pathInfo = req.getPathInfo().substring(1);
		return pathInfo;
	}
	
	// helpers
	/**
	 * Provides a statically accessible reference to the http servlet request
	 * as a thread local. That is, the request object that is active
	 * for the currently executing thread.
	 * @return the http servlet request
	 */
	static public HttpServletRequest getThreadLocalHttpServletRequest() {
		return threadsRequest.get();
	}
	
	/**
	 * Provides a statically accessible reference to the authenticated or 
	 * guest user principal associated with the current thread's http
	 * request. 
	 * @return the user principal
	 * @see PrincipalUtils#getPrincipal(HttpServletRequest)
	 */
	static public Principal getThreadLocalPrincipal() {
		return threadsPrincipal.get();
	}
	
	/**
	 * Load the service.context implementations specified in the web.xml.
	 * @param props - the properties representation of the service.context entries
	 * @param cfg - the servlet config to use for inspecting deployment configurations
	 * @throws ServletException
	 */
	protected void loadServiceContextImplementations(Properties props, ServletConfig cfg) throws ServletException {
		for (Iterator iter = props.keySet().iterator(); iter.hasNext();) {
			String name = (String) iter.next();
			String val = props.getProperty(name);
			try {
				Class clz = Class.forName(val);
				RemoteService impl = (RemoteService) clz.newInstance();
				if (impl instanceof ServletConfigSupport)
					((ServletConfigSupport)impl).setServletConfig(cfg);
				serviceContexts.put(name, impl);
				LOGGER.info("registering service context [" + name + "]=" + impl);
			} catch (Exception err) {
				throw new ServletException("unable to initialise GWT-RPC service [" + val + "]");
			}
		}
	}
	
	/**
	 * Loads the action.context implementations specified in the web.xml. Note
	 * this will also separate those wildcard entries from the qualified 
	 * entries.
	 * @param props - the properties representation of the action.context entries
	 * @param cfg - the servlet config to use for inspecting the deployment config 
	 * @throws ServletException
	 */
	protected void loadActionContextImplementations(Properties props, ServletConfig cfg) throws ServletException {
		for (Iterator iter = props.keySet().iterator(); iter.hasNext();) {
			String name = (String) iter.next();
			String val = props.getProperty(name);
			try {
				Class clz = Class.forName(val);
				Action impl = (Action) clz.newInstance();
				impl.setServletConfig(cfg);
				Map<String, Action> target;
				if (name.endsWith(WILD_CARD_SPECIFIER)) {
					name = name.substring(0, name.length() - 2);
					target = wildCardActionContexts;
				} else
					target = actionContexts;
				target.put(name, impl);
				LOGGER.info("registering action context [" + name + "]=" + impl);
			} catch (Exception err) {
				throw new ServletException("unable to initialise GEMS action [" + val + "]");
			}
		}
	}
	
	static public interface Action extends ServletConfigSupport {
		public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
	}
	
	static public interface ServletConfigSupport {
		public void setServletConfig(ServletConfig ctx) throws ServletException;
	}
}